Repository navigation
Allow explicit wildcard GitHub App repositories in strict mode - #66039
Conversation
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Workflow-global matching can incorrectly exempt an unrelated custom token step with matching ID and credentials.
Review effort: Balanced
Findings: 1
What changed in this PR
Allows strict-mode compilation for explicitly wildcard-scoped GitHub App tokens.
Changes:
- Tracks compiler-generated wildcard token steps.
- Preserves permission validation while exempting wildcard steps from repository-input validation.
- Adds GitHub tool and safe-output regression tests.
| File | Description |
|---|---|
pkg/workflow/safe_outputs_app_config.go |
Records generated wildcard token steps. |
pkg/workflow/compiler.go |
Resets wildcard tracking per compilation. |
pkg/workflow/compiler_types.go |
Adds compiler tracking state. |
pkg/workflow/app_token_permissions_validation.go |
Recognizes tracked wildcard steps during validation. |
pkg/workflow/app_token_permissions_validation_test.go |
Covers wildcard compilation and permission safeguards. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
| return id != "" && clientID != "" && privateKey != "" && | ||
| c.wildcardAppTokenSteps[appTokenStepKey{id, clientID, privateKey}] |
There was a problem hiding this comment.
Fixed in bac8c5f: wildcard exemptions are now scoped to the containing job and matched step metadata, with only one matching step allowed to consume the exemption. Regression tests cover same-job and cross-job spoofing.
|
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅
|
|
✅ Test Quality Sentinel completed test quality analysis. Test Quality Sentinel skipped because pre-fetch PR data was unavailable: unable to fetch test file diff
|
|
✅ Ponytail Reviewer completed successfully! Lean already. Ship.
|
|
✅ PR Code Quality Reviewer completed the code quality review.
|
|
✅ Design Decision Gate 🏗️ completed the design decision gate check. See the comment below for the result and any generated ADR draft.
|
🏗️ Design Decision Gate: ADR RequiredThis PR triggered ADR enforcement and no Architecture Decision Record was found. Why enforcement applies
ADR search results
Draft ADR committedI generated a draft and pushed it to this branch:
Decision captured: the compiler records, at generation time, which Evidence used
Alternatives documented: emitting a literal Next actionReview
|
Comment MemoryPeek at saved memory (pr-code-quality-reviewer)Note This comment is managed by comment memory.Expand the saved memory block to view or edit the persistent context for this thread.
|
There was a problem hiding this comment.
Request changes
The wildcard-app-token exemption still looks unsafe: it is keyed only by step ID plus the raw client-id/private-key strings, so another step in the same workflow can inherit the exemption without ever coming from an explicit repositories: ["*"] config.
Blocking theme
Step IDs are only job-local, and the validator never records job identity or provenance. That means a second actions/create-github-app-token step that reuses the same generated tuple can bypass the repository-scoping check entirely. Strict mode is supposed to reject exactly that class of unscoped token, so this change weakens the guardrail instead of narrowing it.
🔎 Code quality review by PR Code Quality Reviewer · copilot · gpt54 · 36.6 AIC · ⌖ 5.51 AIC · ⊞ 21.1K
Comment /review to run again
There was a problem hiding this comment.
This PR adds a narrow, well-intentioned exemption so explicitly configured wildcard (repositories: ["*"]) GitHub App tokens don't trip the strict-mode "missing repositories input" check. Test coverage for the happy path is good.
However, there's a blocking correctness/security issue: the exemption (hasGeneratedWildcardAppTokenStep) identifies a "compiler-generated" step only by matching its visible id/client-id/private-key fields, which are all values a workflow author can freely reproduce in an unrelated, hand-authored step. This lets any custom step spoof the exemption and skip the repositories scoping requirement entirely — exactly the bypass the safeguard is meant to prevent. See inline comment for details and suggested fix (track provenance of the generated step object/position rather than matching on content).
Scope of review
This is a backend Go compiler/validation change with no UI surface, so the Impeccable UI review modes (audit/critique/harden/distill/extract/clarify) don't apply. Reviewed directly for correctness and security per the fallback guidance.
🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · copilot · sonnet50 · 46.4 AIC · ⌖ 13.4 AIC · ⊞ 8.2K
| return false | ||
| } | ||
| return id != "" && clientID != "" && privateKey != "" && | ||
| c.wildcardAppTokenSteps[appTokenStepKey{id, clientID, privateKey}] |
There was a problem hiding this comment.
Confirmed: the wildcard exemption can be spoofed by an unrelated, custom step.
hasGeneratedWildcardAppTokenStep matches purely on step["id"], with["client-id"], and with["private-key"] — all of which are attacker/author-controlled strings in the compiled workflow, not a reference to the actual generated step object. Any hand-written job (e.g. one added via an import, or directly in the markdown's custom steps:) that reuses the same id: github-mcp-app-token (or safe-outputs-app-token) together with the same client-id/private-key expressions will be misclassified as compiler-generated and skip the repositories requirement in strict mode — even though it has no connection to the actual wildcard-configured GitHub App step.
This defeats the stated safeguard: "Continue requiring explicit repositories input... for unrelated token steps" (see PR description). Since client-id/private-key are typically ${{ vars.APP_ID }} / ${{ secrets.APP_KEY }} — values an author of the workflow already knows/controls — this is trivially copyable, not a secret binding.
Suggested fix: track provenance via an identity that user-authored steps can't fabricate, e.g.:
- Record the
(jobName, step index)or a pointer/marker written into the step map at generation time (e.g. an internal field not emitted to YAML, consulted before stripping), rather than matching on step content, or - Validate/tag compiler-generated steps before they are merged with custom/imported steps, so the exemption is based on "this step object came from the generator" rather than "this step's visible fields happen to match."
@copilot please address this.
There was a problem hiding this comment.
Fixed in bac8c5f: custom steps reusing the generated ID and credential expressions no longer inherit the exemption from another job; duplicate matches in the generated job are also rejected.
There was a problem hiding this comment.
Skills-Based Review 🧠
Applied /codebase-design and /tdd — requesting changes on the job-scoping gap in the new wildcard exemption.
📋 Key Themes & Highlights
Key Themes
- Collision risk in the exemption key:
appTokenStepKey{id, clientID, privateKey}is built from a job-local stepidbut stored in a compiler-wide map (c.wildcardAppTokenSteps), spanning every job in the compiled workflow. A user-authored step in an unrelated job that coincidentally shares the sameid/client-id/private-keycombination as a compiler-generated wildcard step would silently inherit the exemption from strict-mode scoping checks — this was already flagged by an existing Copilot review comment and is worth fixing before merge. - Test coverage is narrow: the new regression tests cover only 2 of ~13 call sites of
buildGitHubAppTokenMintStepWithMeta. The fix is plausibly correct for all of them, but only thegithubtool andsafe-outputspaths are verified.
Positive Highlights
- ✅ Clear problem statement and well-scoped change — only touches the specific
hasExplicitAppTokenRepositoriescheck path, not the broader permission check. - ✅ Good defense-in-depth test (
TestAppTokenPermissionsWildcardStillChecksPermissionsAndOtherSteps) confirming the exemption doesn't bypasspermission-*validation and doesn't leak to unrelated steps with a differentclient-id. - ✅
c.wildcardAppTokenSteps = nilreset inCompileWorkflowDatacorrectly prevents state leaking across compiles of different workflows in batch mode.
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · copilot · sonnet50 · 114.1 AIC · ⌖ 14.8 AIC · ⊞ 10.2K
Comment /matt to run again
| return false | ||
| } | ||
| return id != "" && clientID != "" && privateKey != "" && | ||
| c.wildcardAppTokenSteps[appTokenStepKey{id, clientID, privateKey}] |
There was a problem hiding this comment.
[/codebase-design] The existing Copilot review comment on this line is correct and worth confirming: appTokenStepKey{id, clientID, privateKey}} is keyed only on step id, which is a job-local identifier, but wildcardAppTokenSteps is a single compiler-wide map built across all jobs in the workflow. Any actions/create-github-app-token step in an unrelated custom job (steps:, jobs:) that happens to reuse the same id (e.g. github-mcp-app-token) plus the same client-id/private-key expressions would be silently exempted from the repositories/permission scoping check, even if its own repositories input is missing or broader than intended.
💡 Suggested fix
Scope the key to the job as well, e.g. appTokenStepKey{jobName, id, clientID, privateKey}, and pass jobName down from validateAppTokenPermissions into hasGeneratedWildcardAppTokenStep. This keeps the exemption tied to exactly the compiler-generated step it was computed for, closing the collision window a user-authored step could otherwise exploit to bypass strict-mode scoping.
@copilot please address this.
There was a problem hiding this comment.
Fixed in bac8c5f: the provenance key now includes the containing job, and validation only exempts one matching step. Added tests for cross-job and same-job spoofing.
|
|
||
| func (c *Compiler) buildGitHubAppTokenMintStepWithMeta(app *GitHubAppConfig, permissions *Permissions, fallbackRepoExpr string, ownerSourceRepository string, stepName string, stepID string) []string { | ||
| safeOutputsAppLog.Printf("Building GitHub App token mint step: owner=%s, repos=%d", app.Owner, len(app.Repositories)) | ||
| if len(app.Repositories) == 1 { |
There was a problem hiding this comment.
[/tdd] Side effect (c.wildcardAppTokenSteps[...] = true) is recorded unconditionally every time buildGitHubAppTokenMintStepWithMeta runs with repositories: ["*"], even for call sites where the generated step is never subsequently fed through validateAppTokenPermissions on the same parsedWorkflow pass (e.g. helper call sites used only to produce YAML fragments for embedding elsewhere). The tests added cover the two documented call sites (github tool, safe-outputs) but not the ~10 other callers of this function (steering issue, checkout, plugin installation, dispatch-repository, etc.) that also pass app.Repositories == ["*"].
💡 Suggested test
Add a case (or a table-driven loop) exercising at least one more caller path — e.g. tools.checkout[*].github-app or safe-outputs.dispatch-repository — with repositories: ["*"] in strict mode, to confirm the exemption generalizes correctly rather than only working for the two paths explicitly tested.
@copilot please address this.
There was a problem hiding this comment.
Fixed in bac8c5f: the YAML-fragment builder is side-effect free; wildcard provenance is recorded at job-emission points. Strict-mode coverage now also exercises dispatch-repository safe outputs.
|
@copilot address the following outstanding work in one pass:
Push the necessary fixes, reply to each listed review thread and resolve it when addressed. Ignore feedback already answered or resolved. Use the pr-finisher skill and stop when only human review or CI remains; do not trigger CI. Sous-chef head: 725a850
|
…hub-app-repositories Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
|
🎉 This pull request is included in a new release. Release: |

Strict mode rejects GitHub App tokens configured with
repositories: ["*"]because the compiler correctly omits the action’srepositoriesinput. This blocks documented cross-repository access for GitHub tools and safe outputs.repositoriesinput or suggesting a current-repository restriction.permission-*inputs and repository scoping for unrelated token steps.v0.90.3: strict mode rejects documented github-app.repositories: ["*"] (no explicit repositories input)#65814